Skip to content

feat(lint)!: refuse an object that shows two visible fields under one label - #33639

Queued
objectstack-fleet[bot] wants to merge 7 commits into
mainfrom
claude/project-thread-g9ecx3
Queued

objectstack-fleet[bot] wants to merge 7 commits into
mainfrom
claude/project-thread-g9ecx3

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #33545
Part of #33506

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — the authoring half (an AI builder reusing owner_id rather than adding a second owner field) is the skills lane's on the parent card; os explain field-label-duplicate carries the rule's full text, and four CLI transcripts in content/docs move their author-time rule count from 67 to 68 (claim amendment 6887617666).

Severity ruling: error at the author door, installed apps unaffected at load (claim 6887573651, Ruling-ref 「开放 p3卡认领」). Measured: no boot or registration path runs the author-time rules, so no fork.

维护者速读

  • 改了什么:@objectstack/lint 新增一条门禁规则 field-label-duplicate:同一对象上两个及以上可见字段(含平台注入的 owner_id、创建/修改时间与人)在作者写的标签或任一语言下显示为同一个标签,就报错。os build / os validate / os lint 和元数据发布门(对象写入)都执行它。示例应用里三处中文标签撞了 owner_id 的「负责人」,已改名(showcase 的任务「处理人」、团队「组长」,todo 的任务「处理人」),并登记了 ADR-0087 迁移条目。
  • 为什么改:staging 上 AI 搭的工单对象有 owner_id(中文「负责人」)和新加的 assignee(也叫「负责人」),表格出两列「负责人」、看板卡片显示两次,用户分不清改的是哪个。构建时就拦下,比上线后再发现好。
  • 风险与回滚:今天能构建的元数据如果有同名可见字段会构建失败(这是有意的收紧);已安装的应用启动不受影响。发布门上,已存储对象若带同名字段,再次保存该对象会被拒直到改名;保存别的对象不受影响。回滚即 revert。
  • 先看:packages/lint/src/lint-duplicate-field-labels.ts(比较哪些字段、哪些语言),packages/lint/src/authoring-rules.ts(新登记项,门禁 + 对象发布门),examples/app-showcase/src/system/translations/index.ts(示例改名)。
  • 两分钟验证:pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/lint-duplicate-field-labels.test.ts,预期 9 个用例全绿;其中第一个就是 staging 的 owner_id + assignee「负责人」。
  • 需要你做的:无,席位落地。

What changes

  • packages/lint/src/lint-duplicate-field-labels.ts (new): lintDuplicateFieldLabels(stack), rule id FIELD_LABEL_DUPLICATE = field-label-duplicate, error. Per object it compares the fields the object SERVES: its own fields with every objectExtensions field of the stack folded in (objectsAsServed, tied fields left out, as lintAutonumberFormats does), plus the columns the registry injects on it (injectedColumnDefsFor, the spec tables applySystemFields spreads; an authored field of the same name replaces the injected one). hidden: true fields are skipped; the injected organization_id, owning_business_unit_id and queue are hidden by their own definitions, so owner_id and the four audit columns are compared. Labels are compared trimmed, once as authored and once per locale, through the spec's own read-exit translator translateObject (no second copy of its ladder). Locales: every locale a translations bundle of the stack carries plus BUILTIN_SYSTEM_FIELD_LABEL_LOCALES (en, zh-CN, ja-JP, es-ES), the locales a /meta read translates the injected columns into with no bundle at all. That second set is what catches the AI 搭建新增的「负责人」字段与系统字段 owner_id 同名,表格出现两列「负责人」、看板卡片重复显示 #33506 case: the author's assignee keeps 负责人 while owner_id is served 负责人 to a zh-CN reader. One finding per group of fields sharing a label; it names every field, the label, the locale(s) when only some clash, and the injected column(s) involved; the hint says rename, hidden: true, or drop the duplicate and use the platform column.
  • packages/lint/src/authoring-rules.ts: one entry, tier: 'gating', input: 'parsed', all three commands, surfaces: CLI_AND_RUNTIME, runtimeTypes: ['object'] (the lintSummaryRollups shape). index.ts: barrel export of the function, the id constant and the finding type.
  • packages/lint/src/rule-explanations.ts: os explain field-label-duplicate entry (additive, appended after the last entry and at the end of the table; in-flight PR fix(objectql, lint): synced objects are told to drop write verbs, not to open userActions #33517 also edits this file).
  • User text: none. The family's *.user-text.ts catalogs hold sentences for door ADVISORIES only (lint-autonumber-formats.user-text.ts: "The two errors … refuse the write and have no sentence here"); this rule is an error.
  • Tests: lint-duplicate-field-labels.test.ts (new, 9 cases: the AI 搭建新增的「负责人」字段与系统字段 owner_id 同名,表格出现两列「负责人」、看板卡片重复显示 #33506 pair red, hidden duplicate clean, distinct labels clean, ownership: 'none' clean, a clash only one translated locale creates naming that locale, an authored clash named once with no locale, an authored owner_id with its own label replacing the injected one, the registry entry on all three commands, and the explanation naming exactly the built-in locales); runtime-gate.object-writes.test.ts (the door roster and rulesRun pins gain the entry; a new describe: the write that adds the clash is refused with a name-keyed path, the hidden and relabelled controls publish, a sibling write is not charged with a stored object's clash, and a re-save of the clashing object is judged on its body).
  • Shipped metadata the narrowing bit, relabelled (zh-CN bundle only): example-showcase showcase_task.assignee 负责人 → 处理人 and showcase_team.lead 负责人 → 组长 (plus the p_assignee action-param label and its comment, which said it matches showcase_task.assignee); example-todo todo_task.owner 负责人 → 处理人 (and the todo_tasks_by_owner report label/description that names the same field). Each of the three was served 负责人 beside owner_id to a zh-CN reader.
  • ADR-0087: packages/spec/src/migrations/entries/semantic/18.object-field-label-duplicate-refused.ts (a structured TODO: which field keeps the label is the author's choice, so no D2 conversion); the changeset carries registered object-field-label-duplicate-refused.
  • Fixtures the door now refuses, relabelled (claim amendment 6887847864, no assertion changed): two metadata-protocol test files, both about other rules at the publish door. protocol.runtime-authoring-gate.test.ts: four fixture objects declare owner: { type: 'text', label: 'Owner' }, which the door now reads as one label with the injected owner_id ("Owner"); relabelled 'Owner Name' (lines 413, 703, 714, 791). protocol.unknown-picklist-binding-refusal.test.ts: the object() helper gives every bound select field the label 'Tier', so a fixture binding t and tier showed two "Tier" columns; the label is now `Tier (${field})` (line 187). Neither test is about labels or owner_id; 266 of 266 metadata-protocol tests green after the relabel. No other fixture in the swept suites or the dogfood battery was refused.
  • Docs: the four CLI transcripts' Running author-time rules (67) → (68) (claim amendment 6887617666).
  • .changeset/33545-duplicate-field-labels.md: @objectstack/lint and @objectstack/spec minor (narrowed), BREAKING banner with FROM → TO lines and fixes.

Where the rule runs, measured

  • os build / os validate / os lint and os verify's author-time step: the registry (authoringRulesFor). Scaffold validation runs it too (scaffold-validate.ts).
  • The publish door: saveMetaItem with state: 'active' and the draft→active promotion, through judgeRuntimeAuthoringRules / runtimeAuthoringRulesFor('object'). Studio, REST /meta and MCP/AI authors reach it there. The door snapshot carries no translations, so at the door the authored labels and the built-in system-field labels are compared (which is the staging case).
  • Boot / registration: nothing on that path runs AUTHORING_RULES (git grep over every non-test consumer: the CLI commands, scaffold validate, os verify, the metadata-protocol save/publish gate; metadata-diagnostics.ts imports two named lint functions only, not the registry). So an installed app keeps loading — no fork.
  • "The AI builder's integrity check" (card item 1): not in this repository (no such check under packages/; the builder is in cloud). What this repo offers it is the publish door above, which every MCP/REST/Studio object write passes; nothing was done in cloud.

Tests

  • pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/lint-duplicate-field-labels.test.ts src/runtime-gate.object-writes.test.ts: 53 passed.
  • Ablation 1 (rule returns nothing: if (!isRec(stack)) return findings; → always return, via scripts/ablation-replace.mjs): 6 red, among them the AI 搭建新增的「负责人」字段与系统字段 owner_id 同名,表格出现两列「负责人」、看板卡片重复显示 #33506 owner_id + assignee case, the one-locale clash, the authored clash, the registry entry and both door cases; restored, blob equals HEAD, git diff HEAD empty.
  • Ablation 2 (hidden fields compared: if (def.hidden === true) continue; neutralised): 2 red, the hidden control in the rule test and the hidden/relabelled publish control at the door; restored the same way.
  • Consumer suites on this branch, each exit 0: rest 8393, runtime 10781, objectql 13028, service-automation 3916, service-datasource 1317, plugin-security 6679, client 812, mcp 1174, cloud-connection 1209, verify 437, example-showcase 545, example-todo 249, example-crm 51, cli unit tier 5966, metadata-protocol 266, plugin-approvals rule-runner tests 24, spec migrations 198.

Acceptance notes

  • Sweep. Every example and every first-party app under packages/apps built through the real CLI (os build): only example-showcase and example-todo were refused, the three fields relabelled above; all 11 build clean now. HotCRM (5 packages, 23 objects, 5 bundles): 0 findings, with a positive control (a duplicate of each object's first visible label) firing on 23 of 23 objects, so the probe reached every object.
  • A re-save of a stored object that carries the clash is refused at the publish door until it is fixed, as for every gating rule there (the door's differential removes the written item's stored self, so its own clash is new to the judgment). A write of any other object is never charged with it. The door test pins both.
  • Out of scope, reported: run over the platform's own objects with their generated translation bundles (98 objects, 111 bundles), the rule finds three clashes no author door judges, because these objects are not built through os build and the door snapshot carries no translations: sys_import_job created_at/created_count "Создано" (ru), sys_oauth_resource id/identifier "المعرّف" (ar), sys_user accountable_user_id/manager_id "Responsable" (fr). Each is a reader-facing duplicate column in that locale. Left untouched here (translation pipeline output outside the card); named as a follow-up in the report.
  • Where the door compares locales: the authored labels and the built-in en/zh-CN/ja-JP/es-ES system-field labels, which is the staging case; a clash only a stored translation bundle creates is caught by os build/os validate/os lint, not at the object write.

Verification

  • Gate list from node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 274a4bd: 116 commands, every one run, every one exit 0. Three first exited 3 (prerequisite not met, not a finding) and were rerun after the dependency builds: check-plugin-teardown-shape.mjs --self-test (pinned commit fetched), spec check:skill-examples (client builds), check:dual-build-cjs-loads (dist builds). Log: /mnt/project-files/pm-orders/spec/33545-gates-ran.txt.
  • Typecheck: @objectstack/lint (including its test typecheck), @objectstack/metadata-protocol, @objectstack/spec (tsc --noEmit), example-showcase, example-todo: exit 0.
  • Dogfood battery on 274a4bd (pnpm --filter @objectstack/dogfood test under the verify lock, after building @objectstack/dogfood^...): command exit 0, 467 files passed and 3 skipped, 3950 tests passed and 42 skipped. No fixture was refused.
  • Not measured here: the CI-only rows dispatch-gates names as having no local invocation (Build Core, the Dogfood Regression Gate's merge-queue run, the type-check lanes over CI's whole-workspace filters).

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEkQ5mdmzDcF58D1JdRH4y


Generated by Claude Code

New gating rule field-label-duplicate (lintDuplicateFieldLabels): compares
each object's visible fields, the platform-injected columns included, as
translateObject serves them in the authored pass and in every bundle or
built-in system-label locale. Runs on validate, build, lint and the object
publish door.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5mdmzDcF58D1JdRH4y
…om the zh-CN owner column

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5mdmzDcF58D1JdRH4y
…ner column; register the ADR-0087 entry and changeset

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5mdmzDcF58D1JdRH4y
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 15, 2026
@github-actions

github-actions Bot commented Nov 15, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 19 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/lint/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/validating-metadata.mdx (via AUTHORING_RULES (symbol, a top-level const object), runtimeTypes (symbol, a field of const object AUTHORING_RULES))
  • content/docs/kernel/services-checklist.mdx (via TranslationBundle (symbol, a top-level type))
  • content/docs/protocol/kernel/i18n-standard.mdx (via TranslationBundle (symbol, a top-level type))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/lint/src/index.ts) — pages documenting those are invisible to this run
  • 13 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 172 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 3c0033f9d7e9ee49404db6390a7953eb4bb5152e → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7f5bc4b022981672cd39cdd784677422ddc6491e — the merge of head 1e112d65fd7552e0d844732a20c2db77bbd93404 into base 3c0033f9d7e9ee49404db6390a7953eb4bb5152e, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7f5bc4b022981672cd39cdd784677422ddc6491e && git checkout 7f5bc4b022981672cd39cdd784677422ddc6491e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3c0033f9d7e9ee49404db6390a7953eb4bb5152e 1e112d65fd7552e0d844732a20c2db77bbd93404 && git checkout -B drift-repro 3c0033f9d7e9ee49404db6390a7953eb4bb5152e && git merge --no-ff 1e112d65fd7552e0d844732a20c2db77bbd93404

node scripts/docs-audit/affected-docs.mjs --json 3c0033f9d7e9ee49404db6390a7953eb4bb5152e

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 3c0033f9d7e9ee49404db6390a7953eb4bb5152e → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet objectstack-fleet Bot added the needs:contract-review Marker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides label Nov 15, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · os-project-manager · session_01FEkQ5mecqk8M5q9vjKj6aS

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 274a4bd5bffdcec618998245f7b2ae4b09653728
Local-runs: none
Scope: packages/spec/src/migrations/entries/semantic/18.object-field-label-duplicate-refused.ts + packages/lint/src/lint-duplicate-field-labels.ts, packages/lint/src/authoring-rules.ts, packages/lint/src/index.ts, packages/lint/src/rule-explanations.ts, packages/lint/src/lint-duplicate-field-labels.test.ts, packages/lint/src/runtime-gate.object-writes.test.ts, packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts, packages/metadata-protocol/src/protocol.unknown-picklist-binding-refusal.test.ts, examples/app-showcase/src/system/translations/index.ts, examples/app-todo/src/translations/zh-CN.ts, content/docs/deployment/cli.mdx, content/docs/deployment/validating-metadata.mdx, content/docs/getting-started/build-with-claude-code.mdx, content/docs/ui/react-pages.mdx, .changeset/33545-duplicate-field-labels.md (narrowing face: every path of the diff). Read, not reviewed: translateObject, builtinSystemFieldLabel, localeChain, resolveLocaleAmong and BUILTIN_SYSTEM_FIELD_LABEL_LOCALES in packages/spec/src/system/i18n-resolver.ts; packages/spec/src/data/injected-system-column-provenance.ts; packages/lint/src/object-graph.ts, system-fields.ts and runtime-gate.ts; packages/metadata-protocol/src/protocol-authoring-gate.ts and protocol-authoring-channel.ts; packages/objectql/src/registry.ts (injection order, packaged base); packages/rest/src/rest-server.ts (translateOptionsFor); the three generated platform bundles named in ③.

Inputs read: card #33545 body and all four comments (triage 6886885827, claim 6887573651, amendments 6887617666 and 6887847864, dev report 6888745802); parent #33506 body and all comments; PR #33639 body, its 16-file list and the net diff against origin/main. Check-runs on the head as read: 50 runs, none failed. Completed green: Check Changeset (one of two), Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, PR-body guards, Plan Test Core shards, Spec property liveness, Auto Label, filter. Still running: Build Core, Build Docs, Build UI Deps, the second Check Changeset, Lint · ESLint, Lint · CI, release and engine gates, Lint · docs and code guards, Lint · repo and tooling gates, two scoped lints, Live E2E UI, Plan Test UI shards, Test Core 1 to 8 of 8, all four Type Check lanes. ⛔ This verdict is on the design; the derived gate families (tests, type check, repo gates) had not answered when it was written, and in_progress is not a pass. The seat reads them green before any landing step.

① Derived judgments

  • Accept set, CLI: an object with two or more non-hidden, labelled fields that share a trimmed label, as authored or in any compared locale, now fails os build, os validate, os lint (and the consumers of the same registry: os verify's author-time step, scaffold validation). Right: it is the card's item 1 and the claim's severity line.
  • Accept set, publish door: runtimeTypes: ['object'], so an object write with state: 'active' and the draft to active promotion are judged. Right for the ruling as written. object_extension writes are not judged by this rule (see ④, finding 1).
  • Not narrowed: load. Read in the tree: system columns are injected at registration, no boot or registration path calls the registry of author-time rules, the control-plane install channel (package-author) and the migrate-stored pass return before the rules. "Installed apps unaffected at load" is right.
  • Public surface: @objectstack/lint gains lintDuplicateFieldLabels, FIELD_LABEL_DUPLICATE, DuplicateFieldLabelFinding and one RULE_EXPLANATIONS entry. Additive. Right.
  • @objectstack/spec: one semantic registry entry, no schema, no export, no conversion. Right: no lossless mechanical move exists (which field keeps the label is not recorded), so a TODO-only entry is the correct ADR-0087 shape.
  • Shipped metadata: three zh-CN field labels relabelled in two examples, plus one action-param label, one comment and one report label and description that name the same idea. The last three are not fields the rule judges; they keep the bundle's wording coherent and are inside "shipped example metadata". Accepted.
  • Docs: four transcripts move 67 to 68. Consistent with one added registry entry; the drift gate that pins it was still running.

② Semver level

Metadata-protocol: narrowed takes at least minor on each package the diff moves. The changeset bumps @objectstack/lint minor (the rule, the exports) and @objectstack/spec minor (the registry entry). Both correct. No other released package's source moves: the metadata-protocol hunks are test fixtures, the examples and docs publish nothing. Clause-②: no (narrowing) and Metadata-protocol: narrowed are line-initial in the changeset body and in the PR body, the title carries !, the body carries the BREAKING banner, three FROM to TO lines each with its one-line fix, and exactly one ADR-0087 marker (registered object-field-label-duplicate-refused) that matches the entry's id. The upgrade prescription an author hit by it reads is complete: rename, hidden: true, or drop the duplicate and use the platform column, and the bundle-only case says to translate apart in that bundle. No tracker number in any runtime string or in the changeset. Level and declaration: correct.

③ Boundary flags

  • Dev report open_questions: empty. No deviation declared. File surface: every path is inside claim 6887573651 as widened by 6887617666 (four transcripts, count only: confirmed, one line each) and 6887847864 (fixtures, relabel only: confirmed below).
  • Card item 2 (error or warning for installed objects): recorded in the claim and repeated in the changeset, the rule's module note, the explanation and the registry entry. Answered.
  • Card item 1 names "the one the AI builder's integrity check calls". The dev states that check lives in cloud and nothing was done there. From this repository that cannot be confirmed either way. What the tree does show is that every REST, Studio and MCP object publish passes the door. Escalated as a note for the seat: say on the card whether the cloud integrity check reaches this door or needs its own wiring.
  • Out-of-scope finding (three platform objects clash only in generated bundles): confirmed in the tree at this head, in packages/platform-objects/src/apps/translations/. ru.objects.generated.ts: sys_import_job.created_count and sys_import_job.created_at carry the same label (lines 3580 and 3633). ar.objects.generated.ts: sys_oauth_resource.id and sys_oauth_resource.identifier carry the same label. fr.objects.generated.ts: sys_user.accountable_user_id and sys_user.manager_id are both "Responsable" (lines 100 and 111). None of the six fields is hidden in its object source. It is not this PR's to fix: no author door judges platform objects, and the PR does not make these worse. It does need a card, not only a note: it is a reproducible reader-facing defect (Prime Directive 10), the dev left it "noted, not filed". File it unassigned as a finding for triage to route, p3; the fix is three translated-locale string edits in the platform-objects bundles (hand-written values, pnpm check:i18n gates them), which is platform-objects i18n work and not the lint lane's.

④ Design judgment

Narrowing against the published spec on main: the spec declares a field label as free text with no uniqueness, and main builds an object with two same-label visible fields. The narrowing takes that away at the author door and at the publish door, on purpose and by the card. It does not make a consumer tolerate anything; it is a refusal at the producer, which is the Prime Directive 12 direction.

The hits file, 18.object-field-label-duplicate-refused.ts, sentence by sentence against the code: surface names the three places a label comes from and the code reads exactly those (objects[].fields, objectExtensions[].fields through objectsAsServed, translations[] per locale). replacement names the three fixes the hint prints. "the metadata publish door (422 on an object write)" matches runtimeTypes: ['object']. acceptanceCriteria says a stored object in the shape is still served and its next save through the door is refused: matches the gate (below). The entry carries no relevance, so it lists for every stack in os migrate meta; allowed (the field is optional) and honest. Holds.

Attempts to break it, each read in the cited code:

  • Visibility. "Visible" is the static hidden: true only, on the field as served. Injected organization_id, owning_business_unit_id and queue are hidden: true in the spec's own tables; owner_id and the four audit columns are not, so those five are compared, as the text says. A column the object does not carry is not compared: the rule takes the injected set from injectedSystemColumnDefs, the plan the registry itself injects from (ownership: 'none', systemFields: false, tenancy off), and the ownership: 'none' case is pinned. An authored field under an injected name replaces the injected definition, as at registration. No false positive found on that axis. A field left out of one view is still compared: the rule is object-level and the default surfaces still draw both, which is the ruling.
  • Locales. One authored pass, then one pass per locale: every key of the stack's bundles plus the four built-in system-label locales, through translateObject. Checked against builtinSystemFieldLabel: the read exit translates an injected column whose label still equals its English default by matching the request locale against that table with no bundle at all, so the built-in passes are real reader behaviour and are what catches the parent card's case. Checked against the registry: system columns are injected before the contributor definition is stored, so the packaged base carries owner_id under its English default and the built-in label applies to packaged objects too; the examples' relabels are therefore true positives, not artefacts. A locale with no translation for a field keeps the authored label in the rule and at the read exit alike. A bundle-only clash names its locale (pinned).
  • Door differential. addedFindings in runtime-gate.ts subtracts the baseline (the universe without the written item) and, from the stored universe, only findings located on ANOTHER entry. The rule's path is objects[i].fields on the written entry, so: a write that creates the clash is refused; a sibling write is not charged (its finding is in the baseline); a re-save of an object that already carries the clash is refused. That last behaviour is the gate's documented rule for every gating rule ("a finding located on the written item itself is this write's ... Re-saving a row is writing it"), identical for the roll-up and autonumber rules on the same door, and it is stated in the changeset, the explanation, the module note and the registry entry. It does lock every edit of that one object, an unrelated field included, until the clash is fixed; the fix is available in the same save (relabel or hide) and the refusal names both fields and the three fixes. Other objects, reads, load, the migrate-stored pass and the control-plane install channel are untouched. Honest and consistent.
  • Fixtures. protocol.runtime-authoring-gate.test.ts: four label: 'Owner' to 'Owner Name' on a text field named owner; protocol.unknown-picklist-binding-refusal.test.ts: the helper's 'Tier' to a per-field label. No assertion, expectation or test name changes in either file; no assertion in either file reads a label; the remaining literal 'Tier' at line 248 is an object_extension body the rule does not judge. Neither file holds a deliberate duplicate-label case. Relabel only.
  • New tests. Nine rule cases and three door cases cover the parent card's pair, both controls (hidden, relabelled), ownership: 'none', bundle-only clash, authored clash, authored owner_id, registry wiring on three commands, sibling write, re-save and heal. The explanation test pins the named locales to the spec table, so the prose cannot drift from it.

Findings:

  1. should-fix (a follow-up card discharges it; fixed on this head it earns a delta review). object_extension is a gated door type for other rules, and this rule declares ['object'] only. An extension write that adds a second field under an existing label is published unjudged, which reproduces the parent card's defect through the other object-shaped door. The stored extension is then folded onto its target in the snapshot (objectsAsServed reads objectExtensions), so the target's next object save is charged with a clash the extension created. The PR's text is accurate about its own reach ("an object write"), so nothing shipped is false; the gap is coverage. Card for domain:spec: judge object_extension writes with this rule (the 18.object-extension-door-* entries are the precedent for doing that as its own change).
  2. nit. The rule calls translateObject with the locale alone. The read exit also passes the deployment's fallbackChain and defaultLocale (translateOptionsFor), both readable from stack.i18n. The two agree unless a fallback-locale bundle gives a field a label different from its authored one and the field has no entry in the read locale; then the rule can report a clash a reader with that fallback does not see, or miss one. "Through the same translateObject the /meta read serves them with" is true of the function, not of its options. Follow-up card or accepted deviation; not a correctness risk for the ruled case.
  3. nit. At the door the snapshot carries no translations (the PR says so). Stated consequence: a bundle-only clash is not caught there. Unstated mirror: an authored label that equals a built-in system label in one of the four locales is refused at the door even when the deployment's own bundle translates it apart in that locale, while os build over the same stack passes. Rare (it needs an authored label in one language and a bundle for the same language). One sentence in the explanation would cover it.
  4. nit (prose). visibleWhen is not consulted, and neither is group: two fields with complementary visibleWhen, or the same label under two field groups (a billing and a shipping "City"), are refused. That is inside the ruling (grid headers, export and import columns ignore both), and the sweep found no such case in the examples, the first-party apps or HotCRM, but it is the most likely way an author meets this rule outside the owner case, and neither the changeset nor os explain names it. Add one sentence on a prose face, or leave it.
  5. nit. A field with no label is not compared, on the premise that it is drawn under its unique name. If a surface derives a display label from the name, an unlabelled owner beside owner_id is a clash the rule cannot see. Miss only, never a false finding.
  6. nit. Two fields that clash as authored and are both translated to one other label in a bundle produce two findings for one defect (the group key includes the label). Cosmetic.

No must-fix. No false positive on legitimate metadata was found on the visibility, system-field or locale axes beyond the corners graded above.

Implemented-by: claude/project-thread-g9ecx3
Reviewed-by: session_01FEkQ5mecqk8M5q9vjKj6aS

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · os-project-manager · session_01FEkQ5mecqk8M5q9vjKj6aS

Red read · head 50b105c8 · Test UI (4/16) cancelled · 2026-11-15T04:55Z

Test UI (4/16) (job 132698168775) was cancelled by its 30-minute timeout (2026-11-15T04:24:58Z to 2026-11-15T04:55:17Z); the Test UI roll-up is red only for that. The log shows no failing test: every test file that reported before the cancel passed, the last one (2026-11-15T04:54:24Z) included, and the cancel names no test. The same shard on the reviewed head 274a4bd5 passed in 12 minutes, and this head differs from it only by a merge of main (base sync, K 21). The PR moves no ui/** path; its @objectstack/lint and @objectstack/spec additions are an author-time rule and a registry entry the UI tests do not exercise. Judged a runner-time flake: the seat re-runs the failed jobs once; a second red is treated as real.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · os-project-manager · session_01FEkQ5mecqk8M5q9vjKj6aS

ACCEPT · PR #33639 (#33545) · head 1e112d65fd7552e0d844732a20c2db77bbd93404 (base sync of reviewed head 274a4bd5) · 2026-11-15T05:28Z

席位核对

  • 形态: 16 files, +525/−15 (read from the PR's file list): new author-time rule lintDuplicateFieldLabels (FIELD_LABEL_DUPLICATE, error) in packages/lint, wired into the authoring-rule registry with runtimeTypes: ['object'], its explanation and nine rule cases plus three door cases; one semantic registry entry object-field-label-duplicate-refused in packages/spec; three zh-CN relabels in two examples; relabel-only fixtures in two metadata-protocol tests; four docs transcripts 67 → 68; a minor changeset for @objectstack/lint and @objectstack/spec with the BREAKING banner and Metadata-protocol: narrowed. Not governed (check-governed-merges --pr 33639: 0 of 16 paths, 540 lines): Tier S.
  • 前提实测: no boot or registration path runs the author-time rule registry (measured by the seat before dispatch, re-read by the dev and by the reviewer: the package-author install channel and the migrate-stored pass return before the rules), so installed apps load unchanged and the refusal sits at os build / os validate / os lint and at the publish door for object writes.
  • 门禁 check-runs: on this head 68 runs: 58 success, 10 skipped, 0 failed (Test UI 16 of 16 shards green on this run). mergeable_state: clean.
  • 契约复审: CONTRACT_REVIEW_TIER PASS on the reviewed head 274a4bd5, record 6888857868; this head adds only the two base syncs (no must-fix, one should-fix, five nits). Ruling-ref for the in-seat review: 「你有 fable ,车道契约复审可以自己做」.
  • 接受的偏差: should-fix 1 (an object_extension write is not judged by this rule) is discharged by card lint door: judge object_extension writes with the duplicate-field-label rule, so an extension cannot add a second field under an existing label #33644 (p3, domain:spec, Blocked-by: #33639); the PR's own text says "an object write", so nothing shipped is false. Nits 2 to 6 (fallback-chain options, the door's no-translation mirror case, visibleWhen/group not consulted, unlabelled fields not compared, doubled findings) are accepted as they stand; none is a false refusal of the ruled case.
  • 范围外去向: three platform objects clash only in generated bundles (sys_import_job ru, sys_oauth_resource ar, sys_user fr) → card platform objects: three generated translation bundles give two visible fields of one object the same label (sys_import_job ru, sys_oauth_resource ar, sys_user fr) #33645 filed as a finding, triaged p3 domain:spec and now in flight on this seat. Card item 1's "AI builder integrity check" lives in the cloud repo, which this seat cannot read: every REST, Studio and MCP object publish in this repo passes the door, and whether the cloud check reaches it stays open for the cloud lane.
  • 落地条款: ready + auto-merge now under the maintainer's standing order 「按照 pm-dispatch 规则复核过的都授权」 (2026-10-25T11:35Z) and 「你应该使用 pm-dispatch 技能审核和落地pr」 (2026-11-07T16:59Z). K read on the synced head after its rerun: K = 13 commits on main past PRBASE 3c0033f9 (the Test Core affected-set diff base of this run; < 15, no new language pack since). The reviewed head 274a4bd5 had K = 21 when its CI went green, so the seat merged main in by the relay base sync (no conflict; the PR's own net diff is unchanged, the merge brings in only main's own edits to three of its paths: cli.mdx, validating-metadata.mdx and rule-explanations.ts) and reread the gates. On the first synced head 50b105c8 Test UI (4/16) hit its 30-minute timeout with no failing test (red read posted on this PR); the relay re-run refuses a cancelled run, so the seat took the skill's step ②, a second base sync (1e112d65), and reads its gates here.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 15, 2026 05:28
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 15, 2026
Any commits made after this event will not be merged.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

build validation: report two visible fields with the same label on one object (split from #33506)

1 participant